#include <wm/window.h>
#include <nxos.h>

NX_PRIVATE NX_IpcClient * window_client_ipc = &(NX_IpcClient){};
NX_PRIVATE NX_Bool window_client_init = NX_False;
NX_PRIVATE NX_List clientWindowList;

NX_INLINE NX_IpcClient * GetWindowClient(void)
{
    return window_client_ipc;
}

NX_PRIVATE void DefaultQuitEventHandler(void)
{
    NX_WindowExit();
    NX_ProcessExit(0);
}

NX_PRIVATE NX_WindowQuitEventType UserQuitEventHandler = NX_NULL;

void NX_WindowSetQuit(NX_WindowQuitEventType handler)
{
    UserQuitEventHandler = handler;
}

NX_Error DoWindowPollEvent(NX_Window *win, NX_WindowEvent *e)
{
    if (!window_client_init)
    {
        return NX_EPERM;
    }
    return NX_FifoRead(win->connect.event, (const NX_U8 *)e, sizeof(NX_WindowEvent)) > 0 ? NX_EOK : NX_ENORES;
}

NX_Error NX_WindowPollEvent(NX_Window *win, NX_WindowEvent *e)
{
    NX_Error err = DoWindowPollEvent(win, e);
    if (err != NX_EOK)
    {
        return err;
    }
    if (e->type == NX_QUIT_EVENT)
    {
        if (UserQuitEventHandler != NX_NULL)
        {
            UserQuitEventHandler();
        }
        else
        {
            DefaultQuitEventHandler();
        }
    }
    return err;
}

NX_Error DoWindowWaitEvent(NX_Window *win, NX_WindowEvent *e)
{
    if (!window_client_init)
    {
        return NX_EPERM;
    }
    while (NX_FifoRead(win->connect.event, (const NX_U8 *)e, sizeof(NX_WindowEvent)) <= 0)
    {
        NX_ThreadYield();
    }
    return NX_EOK;
}

NX_Error NX_WindowWaitEvent(NX_Window *win, NX_WindowEvent *e)
{
    NX_Error err = DoWindowWaitEvent(win, e);
    if (err != NX_EOK)
    {
        return err;
    }
    if (e->type == NX_QUIT_EVENT)
    {
        if (UserQuitEventHandler != NX_NULL)
        {
            UserQuitEventHandler();
        }
        else
        {
            DefaultQuitEventHandler();
        }
    }
    return err;
}

NX_PRIVATE NX_Error NX_WindowDirectMap(NX_Window *win, int directMode)
{
    if (!win)
    {
        return NX_EINVAL;
    }

    if (directMode)
    {  
        if (win->oldSurface.pixels != NX_NULL)
        {
            return NX_EAGAIN;
        }
        NX_VideoInfo * info = NX_VideoGetInfo();
        if (!info)
        {
            return NX_EFAULT;
        }
        /* mmap for fb memory */
        void *map = NX_MemMap2(NX_NULL, (void *)info->phyBaseAddr, info->width * info->height * sizeof(NX_Color), NX_PROT_READ | NX_PROT_WRITE);
        if (!map)
        {
            return NX_ENOMEM;
        }
        
        NX_SurfaceWrap(&win->oldSurface, win->connect.surface->pixels, win->connect.surface->width, win->connect.surface->height);

        NX_MemSet(map, 0xff, info->width * info->height * sizeof(NX_Color));
        NX_SurfaceWrap(win->connect.surface, map, info->width, info->height);
        return NX_EOK;    
    }
    else
    {
        if (win->oldSurface.pixels == NX_NULL)
        {
            return NX_EAGAIN;
        }
        
        NX_Surface tmpSurface = *win->connect.surface;
        /* restore */
        NX_SurfaceWrap(win->connect.surface, win->oldSurface.pixels, win->oldSurface.width, win->oldSurface.height);
        /* unmap */
        if (NX_MemUnmap(tmpSurface.pixels, tmpSurface.width * tmpSurface.height * sizeof(NX_Color)) != NX_EOK)
        {
            return NX_ENOMEM;
        }
        win->oldSurface.pixels = NX_NULL;
        win->oldSurface.width = 0;
        win->oldSurface.height = 0;
        return NX_EOK;
    }
}

NX_Window * NX_WindowCreate(int x, int y, int width, int height, NX_Color bg, NX_U32 flags)
{
    NX_IpcMsg *msg = NX_IpcCreateMsg(GetWindowClient(), NX_WINDOW_GET_REQUEST_SIZE(NX_WINDOW_CREATE));
    NX_Request *fr = (NX_Request *)NX_IpcGetMsgData(msg);
    fr->req = NX_WINDOW_CREATE;
    NX_WINDOW_GET_REQUEST(param, NX_WINDOW_CREATE, fr);

    if (!window_client_init)
    {
        return NX_NULL;
    }

    param->x = x;
    param->y = y;
    param->width = width;
    param->height = height;
    param->bg = bg;
    param->flags = flags;

    NX_WindowID ret = NX_IpcCall(GetWindowClient(), msg);
    if (ret < 0)
    {
        NX_IpcDestroyMsg(msg);
        return NX_NULL;
    }

    NX_Window *win = (NX_Window *)NX_MemAlloc(sizeof(NX_Window));
    win->connect = param->connect;
    win->id = ret;
    win->flags = flags;
    NX_SurfaceWrap(&win->oldSurface, NX_NULL, 0, 0);

    win->connect.vmo_map = NX_SOLT_INVALID_VALUE;

    void *addr = NX_ShareMemMap(win->connect.vmo, &win->connect.vmo_map);
    if (!addr)
    {
        NX_IpcDestroyMsg(msg);
        NX_WindowDestroy(win);
        return NX_NULL;
    }

    NX_SurfaceWrap(win->connect.surface, (NX_Color *)addr, win->connect.surface->width, win->connect.surface->height);

    if ((flags & NX_WINDOW_FLAG_FULLSCREEN_DESKTOP) == NX_WINDOW_FLAG_FULLSCREEN_DESKTOP)
    {
        NX_WindowDirectMap(win, 1);
    }

    NX_ListAdd(&win->list, &clientWindowList);

    NX_IpcDestroyMsg(msg);
    return win;
}

NX_Error NX_WindowDestroy(NX_Window *win)
{
    NX_Error err;
    NX_IpcMsg *msg = NX_IpcCreateMsg(GetWindowClient(), NX_WINDOW_GET_REQUEST_SIZE(NX_WINDOW_DESTORY));
    NX_Request *fr = (NX_Request *)NX_IpcGetMsgData(msg);
    fr->req = NX_WINDOW_DESTORY;
    NX_WINDOW_GET_REQUEST(param, NX_WINDOW_DESTORY, fr);

    if (!window_client_init)
    {
        return NX_EPERM;
    }

    param->id = win->id;

    if ((win->flags & NX_WINDOW_FLAG_FULLSCREEN_DESKTOP) == NX_WINDOW_FLAG_FULLSCREEN_DESKTOP)
    {
        NX_WindowDirectMap(win, 0);
    }

    if (win->connect.vmo_map != NX_SOLT_INVALID_VALUE)
    {
        NX_ShareMemUnmap(win->connect.vmo_map);
    }

    NX_ASSERT(win->connect.vmo != NX_SOLT_INVALID_VALUE);
    NX_ShareMemClose(win->connect.vmo);
    NX_ShareMemClose(win->connect.event);
    err = NX_IpcCall(GetWindowClient(), msg);

    NX_ListDel(&win->list);

    NX_MemFree(win);

    NX_IpcDestroyMsg(msg);
    return err;
}

NX_Error NX_WindowGetConfig(NX_Window *win, struct NX_WindowConfig *config)
{
    NX_Error err;
    NX_IpcMsg *msg = NX_IpcCreateMsg(GetWindowClient(), NX_WINDOW_GET_REQUEST_SIZE(NX_WINDOW_CONFIG_GET));
    NX_Request *fr = (NX_Request *)NX_IpcGetMsgData(msg);
    fr->req = NX_WINDOW_CONFIG_GET;
    NX_WINDOW_GET_REQUEST(param, NX_WINDOW_CONFIG_GET, fr);

    if (!window_client_init)
    {
        return NX_EPERM;
    }

    param->id = win->id;

    err = NX_IpcCall(GetWindowClient(), msg);

    *config = param->config;

    NX_IpcDestroyMsg(msg);
    return err;
}

NX_Error NX_WindowSetEventMask(NX_Window *win, NX_U32 event_mask)
{
    NX_Error err;
    NX_IpcMsg *msg = NX_IpcCreateMsg(GetWindowClient(), NX_WINDOW_GET_REQUEST_SIZE(NX_WINDOW_EVENT_MASK_SET));
    NX_Request *fr = (NX_Request *)NX_IpcGetMsgData(msg);
    fr->req = NX_WINDOW_EVENT_MASK_SET;
    NX_WINDOW_GET_REQUEST(param, NX_WINDOW_EVENT_MASK_SET, fr);

    if (!window_client_init)
    {
        return NX_EPERM;
    }

    param->id = win->id;
    param->event_mask = event_mask;

    err = NX_IpcCall(GetWindowClient(), msg);
    NX_IpcDestroyMsg(msg);
    return err;
}

NX_Error NX_WindowShow(NX_Window *win)
{
    NX_Error err;
    NX_IpcMsg *msg = NX_IpcCreateMsg(GetWindowClient(), NX_WINDOW_GET_REQUEST_SIZE(NX_WINDOW_SHOW));
    NX_Request *fr = (NX_Request *)NX_IpcGetMsgData(msg);
    fr->req = NX_WINDOW_SHOW;
    NX_WINDOW_GET_REQUEST(param, NX_WINDOW_SHOW, fr);

    if (!window_client_init)
    {
        return NX_EPERM;
    }

    param->id = win->id;

    err = NX_IpcCall(GetWindowClient(), msg);
    NX_IpcDestroyMsg(msg);
    return err;
}

NX_Error NX_WindowHide(NX_Window *win)
{
    NX_Error err;
    NX_IpcMsg *msg = NX_IpcCreateMsg(GetWindowClient(), NX_WINDOW_GET_REQUEST_SIZE(NX_WINDOW_HIDE));
    NX_Request *fr = (NX_Request *)NX_IpcGetMsgData(msg);
    fr->req = NX_WINDOW_HIDE;
    NX_WINDOW_GET_REQUEST(param, NX_WINDOW_HIDE, fr);

    if (!window_client_init)
    {
        return NX_EPERM;
    }

    param->id = win->id;

    err = NX_IpcCall(GetWindowClient(), msg);
    NX_IpcDestroyMsg(msg);
    return err;
}

NX_Error NX_WindowUpdate(NX_Window *win)
{
    NX_Error err = NX_EOK;

    if (!window_client_init)
    {
        return NX_EPERM;
    }

    if ((win->flags & NX_WINDOW_FLAG_FULLSCREEN_DESKTOP) != NX_WINDOW_FLAG_FULLSCREEN_DESKTOP)
    {
        NX_IpcMsg *msg = NX_IpcCreateMsg(GetWindowClient(), NX_WINDOW_GET_REQUEST_SIZE(NX_WINDOW_UPDATE));
        NX_Request *fr = (NX_Request *)NX_IpcGetMsgData(msg);
        fr->req = NX_WINDOW_UPDATE;
        NX_WINDOW_GET_REQUEST(param, NX_WINDOW_UPDATE, fr);

        param->id = win->id;

        err = NX_IpcCall(GetWindowClient(), msg);
        NX_IpcDestroyMsg(msg);
    }
    return err;
}

NX_Error NX_WindowMove(NX_Window *win, int dx, int dy)
{
    NX_Error err;
    NX_IpcMsg *msg = NX_IpcCreateMsg(GetWindowClient(), NX_WINDOW_GET_REQUEST_SIZE(NX_WINDOW_MOVE));
    NX_Request *fr = (NX_Request *)NX_IpcGetMsgData(msg);
    fr->req = NX_WINDOW_MOVE;
    NX_WINDOW_GET_REQUEST(param, NX_WINDOW_MOVE, fr);

    if (!window_client_init)
    {
        return NX_EPERM;
    }

    param->id = win->id;
    param->dx = dx;
    param->dy = dy;
    err = NX_IpcCall(GetWindowClient(), msg);
    NX_IpcDestroyMsg(msg);
    return err;
}

NX_Error NX_WindowResize(NX_Window *win, int width, int height)
{
    NX_Error err;
    NX_Solt old_vmo;
    NX_Solt old_vmo_map;
    NX_IpcMsg *msg = NX_IpcCreateMsg(GetWindowClient(), NX_WINDOW_GET_REQUEST_SIZE(NX_WINDOW_RESIZE));
    NX_Request *fr = (NX_Request *)NX_IpcGetMsgData(msg);
    fr->req = NX_WINDOW_RESIZE;
    NX_WINDOW_GET_REQUEST(param, NX_WINDOW_RESIZE, fr);

    if (!window_client_init)
    {
        return NX_EPERM;
    }

    param->id = win->id;
    param->width = width;
    param->height = height;
    err = NX_IpcCall(GetWindowClient(), msg);

    if (err != NX_EOK)
    {
        NX_IpcDestroyMsg(msg);
        return err;
    }

    /* record old shm */
    old_vmo = win->connect.vmo;
    old_vmo_map = win->connect.vmo_map;
    
    /* update shm */
    win->connect.vmo = param->connect.vmo;
    win->connect.vmo_map = NX_SOLT_INVALID_VALUE;

    void *addr = NX_ShareMemMap(win->connect.vmo, &win->connect.vmo_map);
    if (!addr)
    {
        /* FXIME: unresize window */
        NX_IpcDestroyMsg(msg);
        return NX_ENOMEM;
    }

    NX_SurfaceWrap(win->connect.surface, (NX_Color *)addr, width, height);
    /* close old shm */
    NX_ShareMemUnmap(old_vmo_map);
    NX_ShareMemClose(old_vmo);

    NX_IpcDestroyMsg(msg);
    return err;
}

NX_VideoInfo * NX_VideoGetInfo(void)
{
    NX_Error err;
    NX_IpcMsg *msg = NX_IpcCreateMsg(GetWindowClient(), NX_WINDOW_GET_REQUEST_SIZE(NX_VIDEO_GET_INFO));
    NX_Request *fr = (NX_Request *)NX_IpcGetMsgData(msg);
    NX_PRIVATE NX_VideoInfo info;
    fr->req = NX_VIDEO_GET_INFO;
    NX_WINDOW_GET_REQUEST(param, NX_VIDEO_GET_INFO, fr);

    if (!window_client_init)
    {
        return NX_NULL;
    }

    err = NX_IpcCall(GetWindowClient(), msg);
    if (err != NX_EOK)
    {
        return NX_NULL;
    }

    info.width = param->info.width;
    info.height = param->info.height;
    info.phyBaseAddr = param->info.phyBaseAddr;
    
    NX_IpcDestroyMsg(msg);
    return &info;
}

typedef NX_Error (*NX_WindowControlHandler)(NX_Window *);

NX_Error NX_WindowControlGroup(NX_RequestID req)
{
    NX_Window *win, *nextWin;
    NX_WindowControlHandler handler;

    switch (req)
    {
    case NX_WINDOW_SHOW:
        handler = NX_WindowShow;
        break;
    case NX_WINDOW_HIDE:
        handler = NX_WindowHide;
        break;
    case NX_WINDOW_DESTORY:
        handler = NX_WindowDestroy;
        break;
    default:
        return NX_EINVAL;
    }

    NX_ListForEachEntrySafe(win, nextWin, &clientWindowList, list)
    {
        handler(win);
    }
    return NX_EOK;
}

NX_Error NX_WindowInit(void)
{
    NX_Error err;
    if (window_client_init)
    {
        return NX_EPERM;
    }

    NX_ListInit(&clientWindowList);

    window_client_init = NX_True;
    err = NX_IpcConnect(NX_WINDOW_IPC_NAME, window_client_ipc, NX_WINDOW_HELPER_STACK_SZIE);
    if (err != NX_EOK)
    {
        return err;
    }

    return NX_EOK;
}

NX_Error NX_WindowExit(void)
{
    NX_Window *win, *nextWin;
    if (!window_client_init)
    {
        return NX_EPERM;
    }

    /**
     * destroy all window
     */
    NX_ListForEachEntrySafe(win, nextWin, &clientWindowList, list)
    {
        NX_WindowDestroy(win);
    }

    window_client_init = NX_False;
    return NX_SoltClose(window_client_ipc->connect);
}

NX_Surface * NX_WindowGetSurface(NX_Window *win)
{
    return win->connect.surface;
}
